--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 0709ce4ba1da72151e6660e232fd05e9750edcd9
Parents : 4ff4a12
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-03-31T03:00:23+03:00
feat(tests): add comprehensive Playwright E2E tests for navigation, shell, and smoke scenarios
Changes
3 files changed, 246 insertions(+), 0 deletions(-)
Diff
diff --git a/tests/e2e/navigation.spec.js b/tests/e2e/navigation.spec.js
new file mode 100644
index 00000000..36f5c82a
--- /dev/null
+++ b/tests/e2e/navigation.spec.js
@@ -0,0 +1,114 @@
+const { test, expect } = require("@playwright/test");
+const { PALETTE_PLACEHOLDER, openCommandPalette, prepareE2eSession } = require("./helpers");
+
+test.describe("Getting started (tutorial page)", () => {
+ test("tutorial route shows welcome copy", async ({ page }) => {
+ await page.goto("/#/tutorial");
+ await expect(page).toHaveURL(/#\/tutorial/);
+ await expect(page.getByRole("heading", { name: /Welcome to\s*MeshChatX/i }).first()).toBeVisible({
+ timeout: 30000,
+ });
+ await expect(
+ page.getByText("The future of off-grid communication", { exact: false }).first(),
+ ).toBeVisible({ timeout: 10000 });
+ });
+});
+
+test.describe("Command palette", () => {
+ test("Ctrl+K opens palette with search field", async ({ page }) => {
+ await page.goto("/#/messages");
+ await openCommandPalette(page);
+ await expect(page.getByPlaceholder(PALETTE_PLACEHOLDER)).toBeVisible();
+ await expect(page.getByText("Navigate", { exact: true })).toBeVisible();
+ });
+
+ test("palette navigates to Settings via filter and Enter", async ({ page }) => {
+ await page.goto("/#/messages");
+ await openCommandPalette(page);
+ const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
+ await input.fill("Settings");
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(/#\/settings/, { timeout: 15000 });
+ await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 20000 });
+ });
+
+ test("Escape closes command palette", async ({ page }) => {
+ await page.goto("/#/messages");
+ await openCommandPalette(page);
+ const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
+ await page.keyboard.press("Escape");
+ await expect(page.getByPlaceholder(PALETTE_PLACEHOLDER)).toBeHidden({ timeout: 5000 });
+ });
+
+ test("palette Getting Started action opens tutorial modal", async ({ page }) => {
+ await page.goto("/#/messages");
+ await openCommandPalette(page);
+ const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
+ await input.fill("Getting Started");
+ await page.keyboard.press("Enter");
+ await expect(page.getByRole("heading", { name: /Welcome to\s*MeshChatX/i }).first()).toBeVisible({
+ timeout: 20000,
+ });
+ });
+});
+
+test.describe("Sidebar and keyboard navigation", () => {
+ test("sidebar links navigate across main sections", async ({ page, request }) => {
+ test.setTimeout(120000);
+ await prepareE2eSession(request);
+ await page.goto("/#/messages");
+ await expect(page).toHaveURL(/#\/messages/);
+
+ const sideNav = page.locator("ul.py-3");
+
+ await sideNav.locator('a[href*="#/nomadnetwork"]').click();
+ await expect(page).toHaveURL(/#\/nomadnetwork/);
+
+ await sideNav.locator('a[href*="#/map"]').click();
+ await expect(page).toHaveURL(/#\/map/);
+
+ await sideNav.locator('a[href*="#/tools"]').click();
+ await expect(page).toHaveURL(/#\/tools/);
+ await expect(page.getByText("Power tools for operators", { exact: true })).toBeVisible({
+ timeout: 20000,
+ });
+
+ await sideNav.locator('a[href*="#/settings"]').click();
+ await expect(page).toHaveURL(/#\/settings/);
+ await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 20000 });
+
+ await sideNav.locator('a[href*="#/about"]').click();
+ await expect(page).toHaveURL(/#\/about/);
+ await expect(page.getByText("MeshChatX", { exact: true }).first()).toBeVisible({ timeout: 20000 });
+ });
+
+ test("Alt+1 jumps to Messages from another route", async ({ page }) => {
+ await page.goto("/#/contacts");
+ await expect(page).toHaveURL(/#\/contacts/);
+ await page.keyboard.press("Alt+1");
+ await expect(page).toHaveURL(/#\/messages/, { timeout: 15000 });
+ });
+
+ test("Alt+S opens Settings", async ({ page }) => {
+ await page.goto("/#/map");
+ await expect(page).toHaveURL(/#\/map/);
+ await page.keyboard.press("Alt+s");
+ await expect(page).toHaveURL(/#\/settings/, { timeout: 15000 });
+ });
+});
+
+test.describe("Tools hub", () => {
+ test("tools index lists utilities heading", async ({ page }) => {
+ await page.goto("/#/tools");
+ await expect(page.getByText("Utilities", { exact: true })).toBeVisible({ timeout: 20000 });
+ await expect(page.getByPlaceholder("Search tools...", { exact: true })).toBeVisible();
+ });
+
+ test("deep link to paper message tool", async ({ page }) => {
+ await page.goto("/#/tools/paper-message");
+ await expect(page).toHaveURL(/#\/tools\/paper-message/);
+ await expect(page.getByRole("heading", { name: "Paper Message Generator", exact: true })).toBeVisible({
+ timeout: 20000,
+ });
+ });
+});
diff --git a/tests/e2e/shell.spec.js b/tests/e2e/shell.spec.js
new file mode 100644
index 00000000..91751a5f
--- /dev/null
+++ b/tests/e2e/shell.spec.js
@@ -0,0 +1,81 @@
+const { test, expect } = require("@playwright/test");
+const { prepareE2eSession } = require("./helpers");
+
+function topChrome(page) {
+ return page.locator("div.sticky.top-0.z-\\[100\\]").first();
+}
+
+test.describe("Shell: sidebar, theme, notifications, call, search", () => {
+ test.beforeEach(async ({ request }) => {
+ await prepareE2eSession(request);
+ });
+
+ test("desktop sidebar collapse toggle changes width", async ({ page }) => {
+ await page.goto("/#/messages");
+ const sidebar = page.locator("div.fixed.inset-y-0.left-0").filter({ has: page.locator("ul.py-3") });
+ await expect(sidebar).toHaveClass(/w-80/);
+ await page.locator("div.hidden.sm\\:flex.justify-end.p-2.border-b button").click();
+ await expect(sidebar).toHaveClass(/w-16/);
+ await page.locator("div.hidden.sm\\:flex.justify-end.p-2.border-b button").click();
+ await expect(sidebar).toHaveClass(/w-80/);
+ });
+
+ test("header theme button toggles dark mode on root shell", async ({ page }) => {
+ await page.goto("/#/messages");
+ const shell = page.locator("#app > div.h-screen.w-full.flex.flex-col").first();
+ await expect(shell).toBeVisible({ timeout: 30000 });
+ const themeBtn = page.getByTitle(/Switch to (light|dark) mode/);
+ await expect(themeBtn).toBeVisible({ timeout: 20000 });
+ const initialDark = await shell.evaluate((el) => el.classList.contains("dark"));
+ await themeBtn.click();
+ await expect.poll(async () => shell.evaluate((el) => el.classList.contains("dark"))).not.toBe(initialDark);
+ await themeBtn.click();
+ await expect.poll(async () => shell.evaluate((el) => el.classList.contains("dark"))).toBe(initialDark);
+ });
+
+ test("notification bell opens panel and closes from header", async ({ page }) => {
+ await page.goto("/#/messages");
+ await topChrome(page)
+ .locator("button")
+ .filter({ has: page.locator('svg[aria-label="bell"]') })
+ .click();
+ await expect(page.getByRole("heading", { name: "Notifications", exact: true })).toBeVisible({
+ timeout: 15000,
+ });
+ await expect(page.getByText("No new notifications", { exact: true })).toBeVisible({ timeout: 10000 });
+ const panel = page.locator("div.fixed").filter({ hasText: "Notifications" }).first();
+ await panel.locator('svg[aria-label="close"]').click();
+ await expect(page.getByRole("heading", { name: "Notifications", exact: true })).toBeHidden({
+ timeout: 5000,
+ });
+ });
+
+ test("call route shows Phone tab", async ({ page }) => {
+ await page.goto("/#/call");
+ await expect(page).toHaveURL(/#\/call/);
+ await expect(page.getByRole("button", { name: "Phone", exact: true })).toBeVisible({ timeout: 20000 });
+ });
+
+ test("messages Announces tab search input accepts text", async ({ page }) => {
+ await page.goto("/#/messages");
+ await page.locator("div.-mb-px.flex").getByText("Announces", { exact: true }).click();
+ const input = page.getByPlaceholder(/Search \d+ recent announces/);
+ await expect(input).toBeVisible({ timeout: 15000 });
+ await input.fill("e2e-filter");
+ await expect(input).toHaveValue("e2e-filter");
+ });
+
+ test("NomadNet favourites and announces search inputs accept text", async ({ page }) => {
+ await page.goto("/#/nomadnetwork");
+ const favSearch = page.getByPlaceholder(/Search \d+ favourites/);
+ await expect(favSearch).toBeVisible({ timeout: 20000 });
+ await favSearch.fill("fav-q");
+ await expect(favSearch).toHaveValue("fav-q");
+
+ await page.locator("button.sidebar-tab").filter({ hasText: "Announces" }).click();
+ const nodeSearch = page.getByPlaceholder(/Search \d+ recent announces/);
+ await expect(nodeSearch).toBeVisible({ timeout: 15000 });
+ await nodeSearch.fill("node-q");
+ await expect(nodeSearch).toHaveValue("node-q");
+ });
+});
diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js
new file mode 100644
index 00000000..401ac3e0
--- /dev/null
+++ b/tests/e2e/smoke.spec.js
@@ -0,0 +1,51 @@
+const { test, expect } = require("@playwright/test");
+
+const E2E_BACKEND_PORT = process.env.E2E_BACKEND_PORT || "8000";
+const E2E_BACKEND_ORIGIN = `http://127.0.0.1:${E2E_BACKEND_PORT}`;
+
+test.describe("MeshChatX E2E (Vite + Python backend)", () => {
+ test("backend /api/v1/status is OK (via Vite proxy)", async ({ request }) => {
+ const res = await request.get("/api/v1/status");
+ expect(res.ok()).toBeTruthy();
+ const body = await res.json();
+ expect(body.status).toBe("ok");
+ });
+
+ test("backend /api/v1/app/info returns version JSON (direct backend)", async ({ request }) => {
+ const res = await request.get(`${E2E_BACKEND_ORIGIN}/api/v1/app/info`);
+ expect(res.ok()).toBeTruthy();
+ const body = await res.json();
+ expect(body.app_info).toBeDefined();
+ expect(String(body.app_info.version).length).toBeGreaterThan(0);
+ });
+
+ test("document title, shell, and app name in header", async ({ page }) => {
+ await page.goto("/");
+ await expect(page).toHaveTitle(/Reticulum MeshChatX/);
+ await expect(page.getByText("Reticulum MeshChatX", { exact: true }).first()).toBeVisible({
+ timeout: 30000,
+ });
+ const root = page.locator("#app");
+ await expect(root).toBeVisible();
+ await expect(root.locator("div").first()).toBeVisible({ timeout: 30000 });
+ });
+
+ test("about page shows MeshChatX and version", async ({ page }) => {
+ await page.goto("/#/about");
+ await expect(page).toHaveURL(/#\/about/);
+ await expect(page.getByText("MeshChatX", { exact: true }).first()).toBeVisible({ timeout: 30000 });
+ await expect(page.locator("#app")).toBeVisible();
+ });
+
+ test("settings route loads profile section", async ({ page }) => {
+ await page.goto("/#/settings");
+ await expect(page).toHaveURL(/#\/settings/);
+ await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 30000 });
+ });
+
+ test("messages route is reachable", async ({ page }) => {
+ await page.goto("/#/messages");
+ await expect(page).toHaveURL(/#\/messages/);
+ await expect(page.locator("#app")).toBeVisible();
+ });
+});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────